require 'fileutils'
require 'cgi'
require 'net/https'
require 'yaml'
require 'highline/import'
load 'deploy'

# The svn repository is used to export the code into the temporary directory before
# uploading code into the Morph control panel. Currently only svn is supported,
# but you could change it to fit your need by changing the get_code task
set :repository, 'https://merc-rorpg.googlecode.com/hg/' # Set here your repository! Example: 'https://www.myrepo.com/myapp/trunk'
set :repo_line_number, __LINE__ - 1 # Needed to report missing repository later on

# The version name to set in the control panel. Defaults to date and time, but can be altered by passing it
# on the command line as -s version_name='The Version Name'
set :version_name, Time.now.utc.strftime('%Y-%m-%d %H:%M:%S')

# If you want to use a different scm or use a different export method, you can change it here
# Please note that the export to directory is removed before the checkout starts. If
# You want it to work differently, change the code in the get_code task
set :deploy_via, :checkout
set :scm, :mercurial

# Default to local dir if it's not defined yet. For local deploy only!!!
set :repository, '.' if scm == :local && (repository.nil? || repository.empty?)

# MORPH SETTINGS, please do not change
set :morph_host, "panel.la-1-5.morphcloud.net"
set :morph_port, 80
set :app_key, "05378ea7966889a31d5271631d62495c49e892cc"
set :app_id, 29
set :mv_cmd, PLATFORM.include?('mswin') ? 'ren' : 'mv'
set :morph_tmp_dir, 'morph_tmp'
set :release_path, morph_tmp_dir # needed in order to generate the correct scm command
set :get_code_using, :get_code
set :req_retries, 3


namespace :morph do

  abort('*** ERROR: You need an application key!') if !exists?(:app_key) || app_key.nil?

  # This is the entry point task. It gets the new code, then upload it into S3
  # to a special folder used later for deployment. Finally it mark the version
  # Uploaded to be deployed
  task :deploy do
    transaction do
      set :app_key, mex_app_key if exists?(:mex_app_key)
      abort('*** ERROR: You need an application key!') if !exists?(:app_key) || app_key.nil?
      update_code
      say("Deployment Queued.\nFor more information on the status of this deployment, you\ncan view the Deployment Logs by clicking the 'View' icon located\non your application's 'Actions' column and by clicking the 'Logs' link in the left side menu.\nIn this same page, you can also view your application's Production and Mongrels logs.")
    end
  end

  # Specialized command to deploy from a packaged gem
  task :deploy_from_gem do
    set :get_code_using, :get_code_from_gem
    deploy
  end

  # This task calls the get_code helper, then upload the code into S3
  task :update_code do
    transaction do
      s3_obj_name = upload_code_to_s3
      say("Creating new application version...")

      #create a version in CP
      req_flds = { 'morph-version-name' => version_name, 'morph-version-s3-object' => s3_obj_name }
      send_request(true, 'Post', morph_host, morph_port, '/applications/0/versions', req_flds, nil, "*** ERROR: Could not create a new version!")
      say("Code Upload Done.")
    end
  end

  # A task that create a temp dir, export the code ouf ot svn and tar
  # the code preparing it for upload.
  # If you are not using svn, or using a different source control tool
  # you can customize this file to work with it. The requirement is that
  # It will export the whole structure into the temp directory as set in
  # morph_tmp_dir.
  #
  # You can choose to release a different version than head by setting the
  # Environment variable 'REL_VER' to the version to use.
  task :get_code do
    on_rollback do
    
      remove_files([morph_tmp_dir, 'code_update.tar.gz'])
    
    end

    # Make sure we have a repo to work from!
    abort("***ERROR: Must specify the repository to check out from! See line #{repo_line_number} in #{__FILE__}.") if !repository

    transaction do
      # Clean up previous deploys
    
        remove_files([morph_tmp_dir, 'code_update.tar.gz'])
    
        #get latest code from from the repository
      
        say("Downloading the code from the repository...")
        system(strategy.send(:command))

        abort('*** ERROR: Export from repository failed! Please check the repository setting at the start of the file') if $?.to_i != 0
        abort("*** ERROR: Rails directories are missing. Please make sure your set :repository is correct!") if Dir.entries("#{morph_tmp_dir}").empty?

        # Verify that we have the expected rails structure
        ['/app', '/public', '/config/environment.rb', '/lib'].each do |e|
          abort("**** ERROR: Missing directory \'#{e}\'. Please make sure to have this folder for us to tell you are deploying a Rails Applications.") if !File.exist?("#{morph_tmp_dir}#{e}")
        end
      

      #create archive
    

      system("tar -C #{morph_tmp_dir} -czf code_update.tar.gz --exclude='./.?*' .")

    
      abort('*** ERROR: Failed to tar the file for upload.') if $?.to_i != 0

      # Verify that we have the expected rails structure in the archive
    
      flist = `tar tzf code_update.tar.gz`
      all_in = flist.include?('lib/') && flist.include?('app/') && flist.include?('config/environment.rb')
    
      abort "***ERROR: code archive is missing the rails directories. Please check your checkout and tar" if !all_in

      remove_files([morph_tmp_dir])
    end
  end

  # Get the code from a packaged gem. Name comes from a setting passed to the command
  # using the -s form
  task :get_code_from_gem do
    # Make sure we have the gem defined and that we have the gem file
    if !exists?(:gem_file) || gem_file.nil?
      abort("***ERROR: The gem file must be provided on the command line using -s.\n          For example: cap -f morph_deploy.rb -s gem_file=/home/morph/my_app.gem morph:deploy_from_gem")
    end

    abort("***ERROR: gem file not found! Please check the file location and try again") if !File.exists?(gem_file)

    # Remove older file
    remove_files(["code_update.tar.gz"])

    # Extract the data.tar.gz code from the gem
    system("tar xf #{gem_file} data.tar.gz")
    abort("***ERROR: Couldn't find the data.tar.gz file in the gem file provided!") if !File.exists?('data.tar.gz')

    # rename it to upload_code.tar.gz
    system("#{mv_cmd} data.tar.gz code_update.tar.gz")
  end

  # A task to get the S3 connection info and upload code_update.tar.gz
  # Assumes another task prepared the tar.gz file
  task :upload_code_to_s3 do

    self.send get_code_using

    abort "*** ERROR: Could not find archive to upload." unless File.exist?('code_update.tar.gz')

    s3_data = ''
    say('Getting upload information')
    send_request(true, 'Get', morph_host, morph_port, '/applications/0/versions/put_link', {}, nil, "*** ERROR: Could not get the Cloud connection data!") do |req, res|
      s3_data = YAML.load(res.body)
    end

    if !s3_data.empty?
      say('Uploading code to cloud...')
      File.open('code_update.tar.gz', 'rb') do |up_file|
        send_request(false, 'Put', s3_data[:host], s3_data[:port], s3_data[:path], s3_data[:header], up_file, "*** ERROR: Could not upload the code!", s3_data[:ssl])
      end
    end

    s3_data[:obj_name]
  end

  # Helper to add the mex auth fields to the requests
  def add_mex_fields(req)
    if exists?(:deploy_key) and app_key
      req['morph-deploy-key'] = deploy_key
    else
      req['morph-user'] = morph_user
      req['morph-pass'] = morph_pass
    end
    req['morph-app']  =  app_key
  end

  # Helper to generate REST requests, handle authentication and errors
  def send_request(is_mex, req_type, host, port, url, fields_hash, up_file, error_msg, ssl = (morph_port == 443 ? true : false))
    tries_done = 0
    res = ''
    while res != :ok
      if is_mex and (!exists?(:deploy_key) or !exists?(:app_key))
        say("*** Getting information for authentication ***") if !exists?(:morph_user) || morph_user.nil? || !exists?(:morph_pass) || morph_pass.nil?
        set(:morph_user, ask("Username/Login: ")) if !exists?(:morph_user) || morph_user.nil?
        set(:morph_pass, ask("Password:  ") { |q| q.echo = false }) if !exists?(:morph_pass) || morph_pass.nil?
      end

      conn_start(host, port, ssl) do |http|
        request = Module.module_eval("Net::HTTP::#{req_type}").new(url)
        add_mex_fields(request) if is_mex
        fields_hash.each_pair{|n,v| request[n] = v} # Add user defined header fields
        # If a file is passed we upload it.
        if up_file
          request.content_length = up_file.lstat.size
          request.body_stream = up_file  # For uploads using streaming
        else
          request.content_length = 0
        end

        begin
          response = http.request(request)
        rescue Exception => ex
          response = ex.to_s
        end

        # Handle auth errors and other errors!
        res = verify_response(request, response, is_mex)
        if res == :ok
          yield(request, response) if block_given?
        elsif exists?(:deploy_key) #if via deploy key, just exit
          say("Incorrect deploy_key or mex_app_key")
          exit
        elsif res != :auth_fail # On auth_fail we just loop around
          tries_done += 1
          abort(error_msg) if tries_done > req_retries
        end
      end
    end
  end


  # Helper to create a connection with all the needed setting
  # And yeals to the block
  def conn_start host = morph_host, port = morph_port, ssl=true
    con = Net::HTTP.new(host, port)
    if ssl
      con.use_ssl = true
      # If you have cert files to use, change this to OpenSSL::SSL::VERIFY_PEER
      # And set the ca_cert of the connection
      con.verify_mode = OpenSSL::SSL::VERIFY_NONE
    end
    con.start
    if block_given?
      begin
        return yield(con)
      ensure
        con.finish
      end
    end
    con
  end

  # Helper to verify if a response is valid. Also handles authentication failures into MeX
  def verify_response(request, response, is_mex)
    res = :ok
    if !response.is_a?(Net::HTTPOK)
      if response.is_a?(Net::HTTPForbidden) && is_mex
        say("Authentication failed! Please try again.")
        set :morph_user, nil
        set :morph_pass, nil
        res = :auth_fail
      else
        if response.is_a?(String)
          say("*** ERROR: connection failure: #{response}")
        else
          output_request_param(request, response)
        end
        res = :failure
      end
    end
    return res
  end

  # Helper to output the results of REST calls to our servers or S3
  def output_request_param(request, response)
    puts "\n========== ERROR TRACE =========="
    puts "+++++++ REQUEST HEADERS +++++++++"
    request.each {|e,r| puts "#{e}: #{r}"}
    puts "+++++++ RESPONSE HEADERS +++++++++"
    response.each {|e,r| puts "#{e}: #{r}"}
    puts "+++++++ RESPONSE BODY +++++++++"
    puts response.body
    puts "+++++++ RESPONSE TYPE +++++++++"
    puts "#{response.class} (#{response.code})"
    puts "========= END OF TRACE ==========\n"
  end

  def remove_files(lists)
    FileUtils.cd(FileUtils.pwd)
    FileUtils.rm_r(lists, :force => true) rescue ''
  end

end
